#include <fs.h>
#include <process.h>
#include <fflags.h>

#include <sys/sysdef.h>

#include <mm/valloc.h>

lnode_t mounts = { .next = &mounts, .prev = &mounts };

void mount_busy(mount_t* mnt) {
    while (mnt) {
        mutex_lock(&mnt->lock);
        mnt->busy_count++;
        mutex_unlock(&mnt->lock);
        mnt = mnt->parent;
    }
}

void mount_free(mount_t* mnt) {
    while (mnt) {
        mutex_lock(&mnt->lock);
        mnt->busy_count--;
        mutex_unlock(&mnt->lock);
        mnt = mnt->parent;
    }
}

int fs_mount(char* target, char* fs_name, device_t* device, int options) {
    dentry_t* mnt_point;
    int errno = fs_path_walk(target, cur_proc->cwd, &mnt_point, NULL, FS_WALK_MKPARENT);
    if (!errno) {
        errno = fs_mount_at(fs_name, device, mnt_point, options);
    }
    return errno;
}

static int do_umount(mount_t* mnt) {
    int errno = 0;
    sb_t* sb = mnt->sb;

    if ((errno = sb->fs->umount(sb))) {
        return errno;
    }

    list_remove(&mnt->list);

    mount_free(mnt->parent);
    fs_free_sb(sb);
    fs_free_dentry(mnt->mnt_point);
    vfree(mnt);

    return errno;
}

int fs_umount(char* target) {
    int errno;
    dentry_t* mnt;

    if (!(errno = fs_path_walk(target, cur_proc->cwd, &mnt, NULL, 0))) {
        errno = fs_umount_at(mnt);
    }

    return errno;
}

mount_t* fs_create_mount(mount_t* parent, dentry_t* mnt_point) {
    mount_t* mnt = valloc(sizeof(mount_t));
    if (!mnt) {
        return NULL;
    }

    list_head_init(&mnt->sub_mnt);
    list_append(&mounts, &mnt->list);
    mutex_init(&mnt->lock);


    mnt->parent = parent;
    mnt->mnt_point = mnt_point;
    mnt->sb = mnt_point->sb;

    if (parent) {
        mount_busy(parent);
        mutex_lock(&mnt->parent->lock);
        list_append(&parent->sub_mnt, &mnt->sub_mnt);
        mutex_unlock(&mnt->parent->lock);
    }

    atomic_fetch_add(&mnt_point->ref_count, 1);

    return mnt;
}

int fs_mount_at(char* fs_name, device_t* device, dentry_t* mnt_point, int options) {
    if (mnt_point->inode && !(mnt_point->inode->type & FS_DIR)) {
        return ENOENT;
    }

    fs_t* fs = fs_find(fs_name);
    if (!fs) {
        return ENODEV;
    }

    if (fs->type == FS_TYPE_READONLY) {
        options |= FS_MNT_READONLY;
    }

    mount_t* mnt = mnt_point->mnt;
    sb_t* sb = fs_alloc_sb();

    sb->dev = device;

    int errno = fs->mount(sb, mnt_point);
    if (!errno) {
        mnt_point->sb = sb;
        sb->fs = fs;
        sb->root = mnt_point;

        mnt_point->mnt = fs_create_mount(mnt, mnt_point);
        if (!mnt_point->mnt) {
            errno = ENOMEM;
            goto clean;
        }
        mnt_point->mnt->flags = options;
    } else {
        goto clean;
    }

    return errno;
clean:
    fs_free_sb(sb);
    return errno;
}

int fs_umount_at(dentry_t* mnt_point) {
    int errno = 0;
    sb_t* sb = mnt_point->sb;
    if (!sb) {
        return EINVAL;
    }

    if (sb->root != mnt_point) {
        return EINVAL;
    }

    if (mnt_point->mnt->busy_count) {
        return EBUSY;
    }

    if (!(errno = do_umount(mnt_point->mnt))) {
        atomic_fetch_sub(&mnt_point->ref_count, 1);
    }

    return errno;
}

int fs_mount_root(const char* fs_name, device_t* device) {
    int errno = 0;
    if (fs_root->mnt && (errno = fs_umount_at(fs_root))) {
        return errno;
    }
    return fs_mount_at(fs_name, device, fs_root, 0);
}

__DEF_SYSCALL4(int, mount, const char*, src, const char*, target, const char*, fs, int, option) {
    int errno = 0;
    dentry_t* dev, * mnt;

    if ((errno = fs_path_walk(src, cur_proc->cwd, &dev, NULL, 0))) {
        goto out;
    }

    if ((errno = fs_path_walk(target, cur_proc->cwd, &mnt, NULL, 0))) {
        goto out;
    }

    if (mnt->ref_count > 1) {
        errno = EBUSY;
        goto out;
    }

    device_t* device = (device_t*)dev->inode->data;

    if (!(dev->inode->type & FS_VOLUME) && !device) {
        errno = ENOTBLK;
        goto out;
    }

    errno = fs_mount_at(fs, device, mnt, option);
out:
    return errno;
}

__DEF_SYSCALL1(int, umount, const char*, target) {
    return fs_umount(target);
}